#!/usr/bin/env python
#coding=utf-8

'''The fanfou.com api library for pyfan.

* NOTE: This document isn't complete and maybe useless. *

Thi api library manage the actions interact with fanfou.com, not only
with fanfou API, but also with web page. It's main functions include:
    * Download timeline for more than 20 status.
    * Upload the newer, extend the older or purge the older timeline.
    * Search within timeline, by keyword, date, or tag.
    * Show by tags, which user post with [] or #xxx etc.
    * Post and delete status.
    * Add and delete favorites.
    * Manage friends and direct message.

To initialize:
    >>> import fanfou
    >>> api = fanfou.Api(username)

    >>> api.status_update()
    >>> api.status_extend(200)
    >>> api.status_purge(200)

    >>> status = api.timeline(start=0, number=20)
    >>> print [u.id for u in statues]

    >>> search = api.search(string="abc", data_start=, data_end=, tags=)
    >>> print [u.text for u in search]

    >>> tags = api.tags()
    >>> print tags
    >>> tag_by = api.search(tags = ["abc", "def"])
    >>> print [u.id for u in tag_by]

    >>> api.set_password(password)
    >>> api.set_email(email_address)

    >>> api.post("Hello world!")
    >>> api.delete(status_id)
    >>> api.add_favorite(status_id)
    >>> api.remove_favorite(favorite_id)

    >>> friends = api.friends()
    >>> print [u.name for u in friends]

    ...
'''

import sys
reload(sys)
sys.setdefaultencoding("utf-8")

import urllib
import urllib2
import time
import base64
import cookielib
import cPickle
import thread
import os.path
import imp
from xml.dom import minidom, Node

import BeautifulSoup

class FanfouError(Exception):
    '''Base Error class.'''

class Status:
    '''Base statue class'''

    def __init__(self,
                 id = None,
                 name = None,
                 text = None,
                 text_xml = None,
                 created_at = None,
                 favorite_id = None):

    self.id = id
    self.name = name
    self.text = text
    self.text_xml = text_xml
    self.create_at = created_at
    self.favorite_id = favorite_id


class Friend:
    '''Base friend class'''

    def __init__(self,
                 id = None,
                 name = None,
                 screen_id = None,
                 location = None,
                 description = None,
                 profile_image_url = None,
                 url = None,
                 protected = None):

    self.id = id
    self.name = name
    self.screen_id = screen_id
    self.location = location
    self.description = description
    self.profile_image_url = profile_image_url
    self.url = url
    self.protected = protected

    @staticmethod
    def new_from_dict(data):
        return Friend(self.id = data.get('id', None),
                      self.name = data.get('name', None),
                      self.screen_id = data.get('screen_id', None),
                      self.location = data.get('location', None),
                      self.description = data.get('description', None),
                      self.profile_image_url = data.get('profile_image_url', None),
                      self.url = data.get('url', None)
                      self.protected = data.get('protected', None))

class Api:
    '''The base interface of fanfou.com.

    * It has many things to rewrite !
    '''

    def __init__(self, user, maxnumber = 100):
        '''Version of the data.

        [[(time), link, content_xml, username], ...]'''

        self.version = "0.0.7"      #version of the fanfou class.
        self.maxnumber = maxnumber        #max number in the list(not exactlly)

        self.user = user            #username
        self.password = ""          #user password
        self.list = []              #user data - see version of data
        self.number = 0             #number of user's post
        self.totalNumber = 0        #number of total number

        self.userdata = {}          #user data
        self.friends = {}           #friend list (id to name)

        self.parallel = 0           #use to get pages
        self.paraError = 0

        if self._load():
            print "Load", self.totalNumber, "posts"
        else:
            self.initialize()

    def __del__(self):
        self._write()
        print "Data saved"

    def _path(self):
        if hasattr(sys, "frozen") or imp.is_frozen("__main__"):
            return os.path.abspath(os.path.dirname(sys.executable))
        return os.path.abspath(os.path.dirname(sys.argv[0]))

    def _write(self):
        selfpath = self._path()
        filename = self.user + ".pyf"
        data = file(os.path.join(selfpath, filename), "wb")
        cPickle.dump(self.version, data, -1)

        cPickle.dump(self.password, data, -1)
        cPickle.dump(self.list, data, -1)
        cPickle.dump(self.userdata, data, -1)
        cPickle.dump(self.friends, data, -1)
        data.close()

    def _load(self):
        try:
            path = self._path()
            filename = self.user + ".pyf"
            fullpath = os.path.join(path, filename)
            data = file(fullpath, "rb")
            version = cPickle.load(data)
            if version != self.version:
                print "Find data, but in old version."
                data.close()
                return False

            self.password = cPickle.load(data)
            self.list = cPickle.load(data)
            self.userdata = cPickle.load(data)
            self.friends = cPickle.load(data)
            self._get_friends_ID()

            self.totalNumber = len(self.list)
            for i in range(0, self.totalNumber):
                if self.list[i][3] == self.user:
                    self.number += 1
            if self.number == 0:
                data.close()
                return False
            data.close()
            print "Loading OK."
            return True
        except IOError:
            print "No data file? Downloading..."
            return False
        except EOFError:
            data.close()
            return False

    def _try_all_post(self, username):
        '''Try to get all the posts.

        Not depend on how many posts write on the site, just try to get all the
        posts of a user.'''

        url = "http://www.fanfou.com/message/" + username
        self.list = []
        i = 1
        lock = thread.allocate_lock()
        self.paraError = 0
        self.parallel = 0

        while 1:
            if self.parallel <= 5:
                pageurl = url + "/p." + str(i)
                print "Getting page", i
                thread.start_new_thread(self._friend_post_list, \
                    (pageurl, self.list, lock))
                if self.paraError >= 5:
                    break
                if len(self.list) >= self.maxnumber:
                    break
                self.parallel += 1
                i += 1
        while 1:
            if self.parallel <= 1:
                break
        self.list.sort()
        self.list.reverse()
        self._remove_duplicate(self.list)
        self.totalNumber = len(self.list)
        self.number = 0
        for i in range(0, self.totalNumber):
            if self.list[i][3] == self.user:
                self.number += 1

    def _remove_duplicate(self, list):
        '''Remove duplicate posts in the list.

        Some times when the program is downloading the list,
        new posts was added at the front. So one post can list
        on two pages.'''

        i = 0
        l = len(list)
        q = 20
        if len(list) < 2:
            return
        while 1:
            if len(list) - i < 20:
                q = len(list) - i
            for t in range(1, q):
                if list[i][1] == list[i+t][1]:
                    list.remove(list[i+1])
                    i-=1
#            try t = list.index(list[i+1]):
#                list.remove(list[t])
#            except ValueError:
#                pass
            i += 1
            if i == len(list) - 1:
                if l - len(list) > 0:
                    self.update()
                break

    def _post_number(self, node):
        '''Get the number of post.

        Try to read it from html,the number on the fanfou site is not quit
        presice, just use it for reference.'''

        if node.nodeType == Node.TEXT_NODE:
            s = node.toxml()
            l = len(s)
            if s[l-3:l] == u"条消息":
                return s[:l-4]

        if node.hasChildNodes:
            for child in node.childNodes:
                q = self._post_number(child)
                if q != None:
                    return q

    def _page_post_list(self, pageurl, lists, lock, number = 20):
        '''Get a post list of a page(self post).

        Get the list from the parsed pages http://fanfou.com/username
        There is a <ol> before the list of posts.'''

        node = minidom.parse(urllib2.urlopen(pageurl))
        l = node.getElementsByTagName("ol")[0]
        try:
            for c in range(0, number):
                temp = (time.strptime(l.childNodes[c].childNodes[1].\
                    firstChild.getAttribute("title"), "%Y-%m-%d %H:%M"), \
                    l.childNodes[c].childNodes[1].firstChild.\
                    getAttribute("href")[10:], \
                    l.childNodes[c].childNodes[0].toxml(), self.user)
                lock.acquire()
                lists.append(temp)
                lock.release()
        except IndexError:
            self.paraError += 1
            self.parallel -=1
            return
        self.parallel -=1

    def _friend_post_list(self, pageurl, lists, lock, number = 20):
        '''Get a post list of a page(self and friend).

        Get the list from the parsed pages http://fanfou.com/username
        There is a <ol> before the list of posts.

        If there is new post writed when the program is caching,
        it well be duplicate.'''

        node = minidom.parse(urllib2.urlopen(pageurl))
        l = node.getElementsByTagName("ol")[0]
        try:
            for c in range(0, number):
                # 时间
                if l.childNodes[c].hasAttribute("class"):
                    continue
                temp = (time.strptime(l.childNodes[c].childNodes[5].\
                    firstChild.getAttribute("title"), "%Y-%m-%d %H:%M"), \
                #print "http://fanfou" + l.childNodes[c].childNodes[1].\
                #    firstChild.getAttribute("href")
                # 链接
                    l.childNodes[c].childNodes[5].firstChild.\
                    getAttribute("href")[10:], \
                # 内容
                    l.childNodes[c].childNodes[3].toxml(), \
                    l.childNodes[c].firstChild.getAttribute("href")[1:])

                lock.acquire()
                lists.append(temp)
                lock.release()
        except IndexError:
            self.paraError += 1
            self.parallel -=1
            return
        self.parallel -=1

    def _add_new_friend(self):
        '''When a new friend is finded.

        what he talks must add to the list.'''
        pass

    def _get_friend_list(self):
        url = "http://api.fanfou.com/users/friends.xml?id=" + self.user
        node = minidom.parse(urllib2.urlopen(url)).firstChild
        self.friends = {}
        items = ('screen_name', 'id', 'name', 'location', 'description', \
            'profile_image_url', 'url', 'protected')
        for child in node.childNodes:
            temp = {}
            for i in items:
                try:
                    temp[i] = child.getElementsByTagName(i)[0].firstChild.data
                except AttributeError:
                    temp[i] = None
            self.friends[temp['id']] = temp 

    def _get_self_information(self):
        url = "http://api.fanfou.com/users/show.xml?id=" + self.user
        node = minidom.parse(urllib2.urlopen(url)).firstChild
        items = ("name", "id", "location", "description","profile_image_url", \
            "friends_count", "followers_count", "favourites_count", \
            "statuses_count")
        for i in items:
            try:
                self.userdata[i] = node.getElementsByTagName(i)[0].firstChild.data
            except AttributeError:
                self.userdata[i] = ""

    def id_to_name(self, id):
        if name == self.userdata['id']:
            return self.userdata['name']
        try:
            return self.friends[id]['name']
        except KeyError:
            return False

    def name_to_id(self, name):
        if name == self.userdata['name']
            return self.userdata['id']
        for i in self.friends:
            if self.friends[i]['name'] == name:
                return i['name']
        return False

    def get_text(self, node):
        '''Get out the Text part out from xml.

        Return a string.'''

        if node.nodeType == node.TEXT_NODE:
            return node.data
        if node.hasChildNodes:
            msg = ""
            for child in node.childNodes:
                msg += self.get_text(child)
            return msg

    def timeline(self, n = 20, s = 1):
        self.print_list(list = self.list, number = n, starts = s)

    def print_list(self, list, name = "", number = 20, starts = 1):
        '''Print out what people said.'''

        if list == []:
            print "Empty list."
            return

        start = starts - 1

        if name != "":
            tempList = []
            try:
                id = self.friendsID[unicode(name)]
            except KeyError:
                id = name
            for i in range(0, self.totalNumber):
                if list[i][3] == id:
                    tempList.append(list[i])
        else:
            tempList = list

        if len(tempList) == 0:
            print "Find nothing"
            return
        if start > len(tempList):
            print "Out of range"
            return
        if (start + number) > len(tempList):
            number = len(tempList) - start

        for i in range(start, start + number):
            q = minidom.parseString(tempList[i][2])
            print i+1,
            if name == "":
                print self.friends[tempList[i][3]][0],
            print self.get_text(q), \
                time.strftime("%Y-%m-%d %H:%M",list[i][0])

    def find_conversation(self, name):
        '''Print conversation beturn user and a friend(give in the name).

        There is u parameter, but have bugs when the name and u are not 
        in the friends list.'''

        fr = "@" + self.userdata["name"]
        to = "@" + name
        l = []
        try:
            o = self.friendsID[unicode(name)]
        except KeyError:
            return l
        for i in range(0, self.totalNumber):
            q = minidom.parseString(self.list[i][2])
            t = self.get_text(q)
            if fr in t and self.list[i][3] == o:
                l.append(self.list[i])
            elif to in t and self.list[i][3] == self.user:
                l.append(self.list[i])
        print "Find", len(l), "posts"
        return l

    def find_post_to_me(self):
        str = "@" + self.user
        return self.find_post_by_string(self.list, str)

    def find_post_by_string(self, list, str):
        '''Find if a string is in the post.

        Use parse every time it find the string, very slow!'''
        l = []
        for i in range(0, len(list)):
            q = minidom.parseString(list[i][2])
            t = self.get_text(q)
            if str in t:
                l.append(list[i])
        print "Find", len(l), "posts"
        return l

    def find_post_by_date(self, list, year = 0, mon = 0, day = 0, hour = -1, min = -1):
        '''Find post by year, month, day, and hour, no minite!

        Return a list of the data.'''
        l = []
        for i in range(0, len(list)):
            if year == 0 or list[i][0][0] == year:
                if mon == 0 or list[i][0][1] == mon:
                    if day == 0 or list[i][0][2] == day:
                        if hour == -1 or list[i][0][3] == hour:
                            if min == -1 or list[i][0][4] == min:
                                l.append(list[i])
        print "Find", len(l), "posts"
        return l

    def _get_password(self):      
        import getpass
        self.password = getpass.getpass("Password: ")
        return self.password

    def _get_email(self):
        self.userdata["email"] = raw_input("Email: ")
        return self.userdata["email"]

    def set_email(self, email):
        '''Set user's login email.'''
        self.email = email

    def set_password(self, password):
        '''Set password in base64 format.'''

        import base64
        b64str = base64.encodestring("%s:%s" % (self.user, password))[:-1]
        self.password = b64str

    def post(self, str):
        '''Post status to fanfou API.
        
        Just post, do not care with the password, if there is some thing
        wrong, raise a error.'''

        if self.password == "":
            raise FanfouError("Post: no password, set first.")
        req = urllib2.Request("http://api.fanfou.com/statuses/update.xml")
        b64str = self.password
        req.add_header("Authorization", "Basic %s" % self.password)
        data = urllib.urlencode({"status":str, "source":"pyfan"})
        try:
            page = urllib2.urlopen(req, data)
            return page
        except urllib2.HTTPError:
            raise FanfouError("Post: wrong password.")

    def delete(self, str):
        '''Delete post form fanfou.com.
        
        Todo: how to make sure if the post is deleted?'''

        if self.password == "":
            self._get_password()
        req = urllib2.Request("http://api.fanfou.com/statuses/destroy.xml?id=" + str)
        req.add_header("Authorization", "Basic %s" % self.password)
        page = urllib2.urlopen(req)
        print "Post Deleted"

    def favorite(self, str, action = "favorite.add"):
        '''Manage favorite.
        
        No api can use? Add by url, you should login and use the cookie.'''

        #Login
        cookie = cookielib.CookieJar()
        opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
        urllib2.install_opener(opener)

        if not self.userdata.has_key("email"):
            self._get_email()
        if self.password == "":
            self._get_password()

        code = urllib.urlencode({'loginname':self.userdata["email"], 'loginpass':self.password, \
           'auto_login':'on', 'action':'login'})
        login = urllib2.Request('http://fanfou.com/login', code)
        opener.open(login)

        #Favorite management
        data = urllib.urlencode({"action":action, "favorite":str})
        page = urllib2.urlopen('http://fanfou.com/shares', data)
        print "Action:", action

    def who(self, list, str = ""):
        '''Find list by username.'''

        if str == "":
            name = self.user
        else:
            try:
                name = self.friendsID[unicode(str)]
            except KeyError:
                return []

        temp = []
        for item in list:
            if item[3] == name:
                temp.append(item)
        print "Find", len(temp), "Posts."
        return temp

    def update(self):
        self.update_self()
        self.update_friends()

        lock = thread.allocate_lock()
        url = "http://www.fanfou.com/message/" + self.user
        i = 1
        k = 0
        temp = []
        num = self.totalNumber

        while 1:
            pageurl = url + "/p." + str(i)
            lists = []
            print "Get page", i
            self._friend_post_list(pageurl, lists, lock)
            for item in lists:
                if item[1] == self.list[0][1] or item[0] < self.list[0][0]:
                    k = 1
                    break
                else:
                    temp.append(item)
            i += 1
            if k == 1:
                break

        self.print_list(temp, number = len(temp))

        temp.reverse()
        for items in temp:
            self.list.insert(0, items)

        self.totalNumber = len(self.list)
        print "Update", self.totalNumber - num, "items"

        self.number = 0
        for i in range(0, self.totalNumber):
            if self.list[i][3] == self.user:
                self.number += 1

        self.parallel = 0
        self.paraError = 0

    def purge(self, num):
        '''Purge old posts by number.'''

        if num > self.totalNumber - 20:
            num = self.totalNumber - 20
        print "Purge", num, "posts"
        while num:
            self.list.pop()
            num -= 1
        self.totalNumber = len(self.list)

    def initialize(self):
        self._try_all_post(self.user)
        print "Get", self.totalNumber, "posts"
        self.update_self()
        self.update_friends()
        print "Get", len(self.friends) - 1 , "friends"
        self._write()

    def update_friends(self):
        self._get_friend_list()

    def update_self(self):
        self._get_self_information()
